package pl.edu.pw.elka.tinyrepo.gate.descriptor;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;

import com.thoughtworks.xstream.XStream;

import pl.edu.pw.elka.tinyrepo.common.exceptions.ContentAlreadyExistException;
import pl.edu.pw.elka.tinyrepo.common.exceptions.WrongPathException;

// FIXME chyba najbardziej brudny kod w projekcie

/**
 * Deskrytpor repozytorium. Przechowuje strukture katalogow repozytorium
 * w postaci deskryptora root katalogu.
 */
public class RepoDescriptor {
   /** Deskryptor glownego katalogu serwera. */
   CatalogDescriptor root = new CatalogDescriptor();
   /** Kolejka priorytetowa sewerow danych wzgl ich zajetosci. */
   DataServerPriorityQueue dsQueue = new DataServerPriorityQueue();
   
   public RepoDescriptor() {
   } 
   
   public RepoDescriptor(File f) {
      if(!f.isFile()) {
         System.err.println("Wrong repo desc. file, creating new repo...");
         try {
            f.createNewFile();
         } catch (IOException e) {
            e.printStackTrace();
            System.err.println("Couldn't create repo file...");
            System.exit(-1);
         }
         return;
      }
      XStream xstr = new XStream();
      try {
         root = (CatalogDescriptor)xstr.fromXML(new FileInputStream(f));
      } catch (Exception e) {
         e.printStackTrace();
         System.err.println("Couldn't read repo desc. file, creating new repo");
         root = new CatalogDescriptor();
      }
   }
   
   /**
    * Zapisuje deskryptor do pliku. 
    * 
    * @param f
    */
   public void writeToFile(File f) {
      System.out.println("Writing descriptor to file: "+f.getAbsolutePath());
      if(!f.isFile()) {
         System.err.println("Couldn't write repo desc file");
         return;
      }
      XStream xstr = new XStream();
      try {
         xstr.toXML(root, new FileOutputStream(f));
         
      }
      catch (Exception e) {
         e.printStackTrace();
         System.err.println("Couldn't write repo desc to xml");
      }
   }
   
   /**
    * Dodaje deskryptor serwera danych do kolejki priorytetowej
    * serwerow danych. 
    * 
    * @param dsc
    */
   public synchronized void addDataServerDescriptor(DataServerDescriptor dsc) {
      if(dsQueue.contains(dsc)) { // FIXME mozna by przeniesc sprawdzanie czy cos jest w kolejce do klasy kolejki
         System.err.println(dsc.getHostName()+" registered one more time...");
         dsQueue.remove(dsc.getHostName());
      }
      long amount = computeUsageForDataServer(dsc.getHostName()); // data serwer moze juz miec jakies dane, a 
      // a dopiero teraz sie rejestruje, np. gdy struktura katalogow jest wczytywana z dysku, a serwer danych rejestruje
      // sie po raz pierwszy
      dsc.setDiskUsage(amount);
      dsQueue.push(dsc); 
   }
   
   /**
    * Sprawdza czy istnieje plik w repo o podanej sciezce.
    * 
    * @param path
    * @return
    */
   public synchronized Boolean containsFile(String path) throws WrongPathException {
      return root.contains(path);
   }
   
   /**
    * Usuniecie deskryptora DSa nie skutkuje zmiana w strukturze katalogow - ta dalej
    * jest, jednak niektore pliki moga odpowiadac
    * @param dsc
    */
   public synchronized void removeDataServerDescriptor(DataServerDescriptor dsc) {
      if(!dsQueue.remove(dsc.getHostName()))
         System.err.println("Tried to remove non existing data server descriptor: "+dsc.getHostName());
   }
   
   /**
    * Delegacja do root directory. 
    * Jezeli jest to plik, to jego rozmiar wplywa na rozmiar deskryptora
    *  
    * @param contentDesc
    * @param path - sciezka wzgledem root katalogu. 
    * @throws FileNotFoundException
    * @throws WrongPathException
    * @throws ContentAlreadyExistException
    */
   public synchronized void addFileOrDirectoryDesc(Descriptor contentDesc, String path) throws FileNotFoundException, WrongPathException, ContentAlreadyExistException {
      if(contentDesc.getClass() == FileDescriptor.class) { 
         FileDescriptor fd = (FileDescriptor)contentDesc; // kod wykonywany zazwyczaj przez DataServer, gdy adnotowany jest nowy plik do bramy
         DataServerDescriptor ds = dsQueue.get(fd.getDestination());
         if(ds == null) { // jezeli nie ma podanego DSa w kolejce, to znaczy ze DS sie nie zarejestrowal - podnies wyjatek
           System.err.println("DS not registered when trying add new file to Gate");
         }
         else {
            ds.setDiskUsage(ds.getDiskUsage()+fd.getSize()); // zwiekszenie odpowiednio zajetosci danych przez DataServer
            dsQueue.repair(); // zmieniono stan deskryptora ds, trzeba kolejke zaktualizowac
         }
      }
      root.addChildDescriptor(contentDesc, path);
   }
   
   /**
    * @param path - sciezka wzgledem root katalogu.
    * @return
    * @throws FileNotFoundException
    * @throws WrongPathException
    */
   public synchronized Descriptor getFileOrDirectoryDesc(String path) throws FileNotFoundException, WrongPathException {
      return root.getChildDescriptor(path); // Deskryptory tez musza miec synchronizowany dostep!!!
   }
   
   /**
    * Funkcja wywolana dla katalogu usuwa go oraz wywoluje
    * metody serwera danych aby usunac z nich pliki. 
    * 
    * @param path
    * @throws FileNotFoundException
    * @throws WrongPathException
    */
   public synchronized void removeFileOrDirectoryDesc(String path) throws FileNotFoundException, WrongPathException {
      Descriptor contentDesc = root.getChildDescriptor(path); // wydobadz element ktory ma byc usuniety
      if(contentDesc.getClass() == FileDescriptor.class) {
         FileDescriptor fd = (FileDescriptor)contentDesc;
         DataServerDescriptor ds = dsQueue.get(fd.getDestination());
         if(ds == null) { // jezeli DSa nie ma w kolejce priorytetowej, tzn ze nie jest zarejestrowany, a to oznacza, ze najprawdopodobniej dane 
                        // zostaja nieusuniete pliki na DSie.
            System.err.println("Removing file while destination is not reachable, it may produce some junk on data servers... ");
         }
         else {
            ds.setDiskUsage(ds.getDiskUsage()-contentDesc.getSize()); // zmniejszanie zajetosci dysku przez serwer
            dsQueue.repair();
         }
      }
      else if(contentDesc.getClass() == CatalogDescriptor.class) { // jezeli jest to katalog, to trzeba zaktualizowac
                                                                   // kolejke serwerow (ich zajetosc) wzgledem kazdego elementu 
                                                                   // nalezacego do wskazanego katalogu. 
         updateQueueForCatalogDelete((CatalogDescriptor)contentDesc); // FIXME rekursywnie zmienia rozmiar serwerow danych
                                                              // posiadajacy elementy nalezace do usuwanego katalogu - sprobowac
                                                              // tak zorganizowac strukture RepoDescriptor, zeby usuniecie 
                                                              // katalogu powodowalo automatyczna aktualiacje deskryptorow serwerow
                                                              // danych
         
      }
      root.removeChildDescriptor(path);
   }
   
   /**
    * Przesuwa dany plik z jednego miejsca w drugie.
    * 
    * @throws ContentAlreadyExistException 
    * @throws WrongPathException 
    * @throws FileNotFoundException 
    */
   public synchronized void moveFileOrDirectoryDesc(String src, String dest) throws FileNotFoundException, WrongPathException, ContentAlreadyExistException {
      root.moveChildDescriptor(src, dest);
   }
   
   /**
    * TODO Tworzy tylko dowiazanie symboliczne!
    * FIXME Dowiazanie symboliczne nie bedzie usuniete przy wywolanie dropFile bramy!
    * ale poki co to moze tak zostac
    * 
    * @param src
    * @param dest
    * @throws FileNotFoundException
    * @throws WrongPathException
    * @throws ContentAlreadyExistException
    */
   public synchronized void copyFileOrDirectoryDesc(String src, String dest) throws FileNotFoundException, WrongPathException, ContentAlreadyExistException {
      root.copyChildDescriptor(src, dest);
   }
   
   public synchronized DataServerDescriptor getBestDataServer() {
      return dsQueue.peek();
   }
   
   
   
   // move, copy 
   
   
   /**
    * Uwaga: rekurencyjne przeszukiwanie wszysktich elementow repozytorium, moze byc potencjalnym
    * waskim gardlem, dlatego trzeba rzadko stosowac ta funkcje. 
    * 
    * @param host
    * @return
    */
   private Long computeUsageForDataServer(String host) {
         return CatalogDescriptor.computeDiskUsageForDestination(host, root);
   }
   
   private void updateQueueForCatalogDelete(CatalogDescriptor catalog) {
      for(Descriptor d : catalog.getChilds().values()) {
         if(d.getClass() == FileDescriptor.class) {
            FileDescriptor fd = (FileDescriptor) d;
            DataServerDescriptor ds = dsQueue.get(fd.getDestination());
            if(ds == null) // jezeli DSa nie ma w kolejce priorytetowej, tzn ze nie jest zarejestrowany, a to oznacza, ze najprawdopodobniej dane 
                           // zostaja nieusuniete pliki na DSie.
               // TODO rozwazyc, czy przypadkiem nie reagowac na to inaczej
               System.err.println("Removing file while destination "+fd.getDestination()+" is not reachable, it may produce some garbage on data servers... ");
            try {
               boolean r = ds.getDsStub().removeFile(fd.getId().toString());
               if(!r) {
                  System.err.println("Couldn't remove file from DS: "+ds.getHostName()+" is this symbolic link?");
               }
               else System.out.println("Removed file: "+fd.getId()+" from:"+ds.getHostName());
            } catch (RemoteException e) {
               e.printStackTrace();
               System.err.println("Removed file while DS disconnected: it may produce some garbage.");
            }
            ds.setDiskUsage(ds.getDiskUsage()-fd.getSize()); // zmniejszanie zajetosci dysku przez serwer
            dsQueue.repair();
         }
         else if(d.getClass() == CatalogDescriptor.class) {
            updateQueueForCatalogDelete((CatalogDescriptor)d);
         }
      }    
   }
}
